package agent.strategy;
import java.util.ArrayList;
import java.util.Collections;

import agent.AgentTile;

/**
 * BoardMemory provides a place to store AgentTiles seen by the agent across rounds.
 * It also provides summary information for how AgentTiles change.
 *
 */
public class BoardMemory {

	// AgentTile[rows][cols][round]
	private AgentTile[][][] memory;
	
	// Total hack
	// double[row][col][0,1,2,3,4]
	// 0 = last seen round
	// 1 = last seen value
	// 2 = poor
	// 3 = average
	// 4 = good
	private double[][][] changeEstimates;
	
	private int currentRound;
	private int numRows;
	private int numColumns;
	private boolean staleChangeEstimates;
	
	/**
	 * Keep track of crap for stats.
	 */
	//TODO I include terminal tiles in with tiles when calculating these; I shouldn't.
	private int exploredTilesThisRound;
	private double bestTileReward = -10.0;
	private double worstTileReward = -10.0;
	private double averageTileReward = -10.0;
	private double totalTileReward = 0;
	private double totalTiles = 0;
	private double bestTerminalReward = -10.0;
	private double worstTerminalReward = -10.0;
	private double averageTerminalReward = -10.0;
	private double totalTerminalReward = 0;
	private double totalTerminalTiles = 0;
	
	public BoardMemory(int numRows, int numColumns) {
		this.numRows = numRows;
		this.numColumns = numColumns;
		this.currentRound = 0;
		this.memory = new AgentTile[numRows][numColumns][10];
		this.changeEstimates = new double[numRows][numColumns][5];
		this.exploredTilesThisRound = 0;
		this.staleChangeEstimates = false;
		// Init Stats?
	}

	public AgentTile getTile(int row, int column) {
		return getTileFromMemory(row, column, this.currentRound);
	}
	
	/**
	 * round is 0-indexed.
	 */
	public AgentTile getTileFromMemory(int row, int column, int round) {
		// Return a copy.
		if (memory[row][column][round] != null) {
			return memory[row][column][round].deepCopy();
		}
		return null;
	}
	
	/**
	 * Important stuff to say about this:
	 * Only tiles that we haven't seen yet will be added. Tiles that have
	 * been seen before will be ignored. The exception to this rule is for
	 * terminal tiles, they can be re-added to memory (in order to allow
	 * updates to reward).
	 * @param tile The AgentTile to add. 
	 */
	public void addTile(AgentTile tile) {
		int row = tile.row;
		int col = tile.column;
		AgentTile referencedTile = memory[row][col][this.currentRound];
		
		// We can only add tiles that haven't been added yet.
		// Or we can update tiles that are terminal states
		if (referencedTile == null) {
			// Add new tile.
			memory[row][col][this.currentRound] = tile.deepCopy();
			this.exploredTilesThisRound++;
			this.staleChangeEstimates = true;
			// Update Stats.
		} else if (referencedTile.isTerminalState) {
			// Update terminal tile.
			memory[row][col][this.currentRound] = tile.deepCopy();
			this.exploredTilesThisRound++;
			this.staleChangeEstimates = true;
			// Update Stats.
		}
		
		if (tile.isTerminalState) {
			if (tile.reward > this.bestTerminalReward) {
				this.bestTerminalReward = tile.reward;
			} else if (tile.reward < this.worstTerminalReward) {
				this.worstTerminalReward = tile.reward;
			}
			this.totalTerminalTiles++;
			this.totalTerminalReward += tile.reward;
			this.averageTerminalReward = this.totalTerminalReward / this.totalTerminalTiles;
		} else if (!tile.isWall){
			if (tile.reward > this.bestTileReward) {
				this.bestTileReward = tile.reward;
			} else if (tile.reward < this.worstTileReward) {
				this.worstTileReward = tile.reward;
			}
			this.totalTiles++;
			this.totalTileReward += tile.reward;
			this.averageTileReward = this.totalTileReward / this.totalTiles;
		}
	}
	
	public void nextRound() {
		this.currentRound++;
		this.exploredTilesThisRound = 0;
		// Update Stats.
	}
	
	/**
	 * This might be tricky.
	 * Can return a beta distribution (probably) for wall and terminal.
	 * But who knows what the distribution looks like for the rewards.
	 */
	public double guessTileReward(int row, int column) {
		if (changeEstimates[row][column][1] == -10) {
			return this.averageTileReward;
		} else {
			int roundDiff = (int) (this.currentRound - changeEstimates[row][column][0]);
			//System.out.println(roundDiff);
			double currentEstimate = changeEstimates[row][column][1] + (roundDiff * changeEstimates[row][column][3]);
			return currentEstimate;
		}
	}
	
	public void guessTileWall(int r, int c) {
		//Return BetaDistribution?
	}
	
	public void guessTileTerminal(int r, int c) {
		//Return BetaDistribution?
	}
	
	public void calculateChangeEstimates() {
		double totalChange = 0;
		double totalRounds = 0;
		double averageChange = 0;
		double bestChange = 0;
		double worstChange = 0;
		double lastValue = -10;
		double lastSeenRound = -1;
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				for (int k = 0; k <= this.currentRound; k++) {
					//System.out.println(memory[i][j][k]);
					AgentTile tempTile = memory[i][j][k];
					if (tempTile != null && !tempTile.isWall) {
						if (lastSeenRound != -1) {
							double change = (tempTile.reward - lastValue);
							totalChange += change;
							totalRounds += (k - lastSeenRound);
							if(change < worstChange) {
								worstChange = change;
							} else if(change > bestChange) {
								bestChange = change;
							}
						}
						if (totalRounds > 0) {
							averageChange = totalChange / totalRounds;
						}
						lastValue = tempTile.reward;
						lastSeenRound = k;
					} else {
						//System.out.println("["+i+","+j+","+k+"] " + tempTile.reward);
					}
				}
				changeEstimates[i][j][0] = lastSeenRound;
				changeEstimates[i][j][1] = lastValue;
				changeEstimates[i][j][2] = worstChange;
				changeEstimates[i][j][3] = averageChange;
				changeEstimates[i][j][4] = bestChange;
				totalChange = 0;
				totalRounds = 0;
				averageChange = 0;
				bestChange = 0;
				worstChange = 0;
			}
		}
		this.staleChangeEstimates = false;
	}
	
	//TODO I set average reward vars in here, but I shouldn't
	//If (cumulative) average is for all rounds up to round, otherwise just the round specified by round.
	public double getAverageObserved(int round, boolean cumulative, boolean includeTerminals) {
		double totalReward = 0.0;
		double terminalReward = 0.0;
		int tilesScanned = 0;
		int terminalTilesScanned = 0;
		int startRound = round;
		AgentTile tempTile;
		
		if (cumulative) {
			startRound = 0;
		}
		
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				for (int k = startRound; k <= round; k++) {
					tempTile = memory[i][j][k];
					// We are only interested in tiles we have seen.
					if (tempTile != null) {
						if (!tempTile.isTerminalState) {
							totalReward += tempTile.reward;
							tilesScanned++;
						} else if (includeTerminals) {
							totalReward += tempTile.reward;
							terminalReward += tempTile.reward;
							tilesScanned++;
							terminalTilesScanned++;
						}
					}
				}
			}
		}
		
		this.averageTileReward = (totalReward / tilesScanned);
		if (terminalTilesScanned != 0) {
			this.averageTerminalReward = (terminalReward / terminalTilesScanned);
		}
		return (totalReward / tilesScanned);
	}
	
	/**
	 * Don't use this; it is brokeded.
	 * @param round
	 * @param cumulative
	 * @param includeTerminals
	 * @return
	 */
	public double getAveragedObservedWithoutOutliers(int round, boolean cumulative, boolean includeTerminals) {
		// Ugh, do I really have to calculate standard deviation?
		// Lets just do Median Absolute Deviation: http://en.wikipedia.org/wiki/Expected_deviation
		ArrayList<Double> rewards = new ArrayList<Double>();
		ArrayList<Double> deviations = new ArrayList<Double>();
		double totalReward = 0.0;
		int startRound = round;
		AgentTile tempTile;
		
		if (cumulative) {
			startRound = 0;
		}
		
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				for (int k = startRound; k <= round; k++) {
					tempTile = memory[i][j][k];
					// We are only interested in tiles we have seen.
					if (tempTile != null) {
						if (!tempTile.isTerminalState) {
							rewards.add(tempTile.reward);
						} else if (includeTerminals) {
							rewards.add(tempTile.reward);
						}
					}
				}
			}
		}
		
		Collections.sort(rewards);
		int medianIndex = new Double(Math.floor(rewards.size() / 2)).intValue();
		double median = rewards.get(medianIndex);
		
		// Find deviation from median.
		for (Double reward : rewards) {
			deviations.add(Math.abs(reward - median));
		}
		Collections.sort(deviations);
		int deviationMedianIndex = new Double(Math.floor(deviations.size() / 2)).intValue();
		double deviationMedian = deviations.get(deviationMedianIndex);
		
		// Remove values too far from median.
		for (Double reward : rewards) {
			if (Math.abs(reward - median) > deviationMedian) {
				rewards.remove(reward);
			}
		}
		
		for (Double reward : rewards) {
			totalReward += reward;
		}
		return (totalReward / rewards.size());
	}
	
	public void fillInAccurateBoard(double[][] reward) {
		if (this.staleChangeEstimates) {
			this.calculateChangeEstimates();
		}
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				if (changeEstimates[i][j][1] == -10) {
					reward[i][j] = this.averageTileReward;
				} else {
					int roundDiff = (int) (this.currentRound - changeEstimates[i][j][0]);
					//System.out.println(roundDiff);
					double currentEstimate = changeEstimates[i][j][1] + (roundDiff * changeEstimates[i][j][3]);
					reward[i][j] = currentEstimate;
					if (memory[i][j][this.currentRound] != null) {
						reward[i][j] = memory[i][j][this.currentRound].reward;
					}
				}
			}
		}	
	}
	
	public void fillInPoorBoard(double[][] reward) {
		if (this.staleChangeEstimates) {
			this.calculateChangeEstimates();
		}
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				if (changeEstimates[i][j][1] == -10) {
					reward[i][j] = this.worstTileReward;
				} else {
					int roundDiff = (this.currentRound - new Double(changeEstimates[i][j][0]).intValue());
					double currentEstimate = changeEstimates[i][j][1] + (roundDiff * changeEstimates[i][j][2]);
					reward[i][j] = currentEstimate;
				}
			}
		}
	}
	
	public void fillInOptimisticBoard(double[][] reward) {
		if (this.staleChangeEstimates) {
			this.calculateChangeEstimates();
		}
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				if (changeEstimates[i][j][1] == -10) {
					reward[i][j] = this.bestTileReward;
				} else {
					int roundDiff = (this.currentRound - new Double(changeEstimates[i][j][0]).intValue());
					double currentEstimate = changeEstimates[i][j][1] + (roundDiff * changeEstimates[i][j][4]);
					reward[i][j] = currentEstimate;
				}
			}
		}
	}
	
	// Break up what we know about seen walls and terminals into easily digestible form.
	public void fillInWallsAndTerminals(boolean[][] walls, boolean[][] activeTerminals,
			boolean[][] pastTerminals, boolean[][] exploredActive, boolean[][] exploredPast) {
		AgentTile tempTile;
		
		// As a great man once said: "To assume makes an ass out of you and Uma Thuman."
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				walls[i][j] = false;
				activeTerminals[i][j] = false;
				pastTerminals[i][j] = false;
				exploredActive[i][j] = false;
				exploredPast[i][j] = false;
			}
		}
		
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				// Look at past rounds.
				for (int k = 0; k < this.currentRound; k++) {
					tempTile = memory[i][j][k];
					if (tempTile != null) {
						exploredPast[i][j] = true;
						if (tempTile.isTerminalState) {
							pastTerminals[i][j] = true;
						}
					}
				}
				// Look at the current round.
				tempTile = memory[i][j][this.currentRound];
				if (tempTile != null) {
					exploredActive[i][j] = true;
					if (tempTile.isTerminalState) {
						activeTerminals[i][j] = true;
					}
					if (tempTile.isWall) {
						walls[i][j] = true;
					}
				}
			}
		}
	}
	
	//TODO This should scale the explore value based on how long ago we last
	// saw a tile.
	public void fillInExploreValues(double[][] exploreReward) {
		AgentTile tempTile;
		
		// Don't put that array in your mouth, you don't know where it's been!
		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				exploreReward[i][j] = 0;
			}
		}

		for (int i = 0; i < this.numRows; i++) {
			for (int j = 0; j < this.numColumns; j++) {
				// We aren't interested in tiles we HAVE seen
				tempTile = memory[i][j][this.currentRound];
				if (tempTile == null) {
					exploreReward[i][j] = 1;
				}
			}
		}
	}
	
	public boolean isTerminal(int row, int col) {
		return this.memory[row][col][this.currentRound].isTerminalState;
	}
	
	public boolean wasTerminal(int row, int col) {
		// Look through previous rounds
		for (int i = 0; i < this.currentRound; i++) {
			if (this.getTileFromMemory(row, col, i).isTerminalState) {
				return true;
			}
		}
		// Or, if we found no previous terminals.
		return false;
	}
	
	/**
     * returns the number of tiles that are explored
     * @return The number of tiles revealed
     */
    public int totalNumExposed(){
    	return this.exploredTilesThisRound;
    }

}
